﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Text;
using DomainDriver.DomainModeling.Constraints;
using DomainDriver.DomainModeling.DataProviders;
using DomainDriver.DomainModeling.DomainObjects;
using DomainDriver.DomainModeling.Exceptions;
using DomainDriver.DomainModeling.Repositories;
using DomainDriver.DomainModeling.TransactionScripts;
using DomainDriver.DomainModeling.Events;

namespace DomainDriver.DomainModeling.DomainModels
{
    public abstract class DomainModelBase : IDomainModel, IBatchPersistableDomainModel
    {
        protected InvalidCallAction m_InvalidCallAction;
        private IDataProvider m_DataProvider;
        private Dictionary<DataSourcePersistenceType, Type> m_DataSourceTypesForPersistenceTypes;
        private Dictionary<string, object> m_Repositories;
        private Dictionary<string, IConstraint> m_Constraints;

        public DomainModelBase(DataSourcePersistenceType persistenceType, InvalidCallAction invalidCallAction, bool buildModelWithConstraints)
        {
            m_InvalidCallAction = invalidCallAction;
            m_DataSourceTypesForPersistenceTypes = new Dictionary<DataSourcePersistenceType, Type>();
            m_Repositories = new Dictionary<string, object>();
            m_Constraints = new Dictionary<string, IConstraint>();

            LoadPersistenceConfiguration(m_DataSourceTypesForPersistenceTypes);

            Type dataSourceType = DataSourceTypesForPersistenceTypes[persistenceType];
            object dataSource = CreateDataSource(persistenceType);
            m_DataProvider = new DataProvider(persistenceType, dataSourceType, dataSource);

            CreateRepositories(m_Repositories);

            if (buildModelWithConstraints)
            {
                CreateConstraints(m_Constraints);
            }

            PerformSetup();
        }

        protected abstract void LoadPersistenceConfiguration(IDictionary<DataSourcePersistenceType, Type> dataSourceTypesForPersistenceTypes);

        public abstract object CreateDataSource(DataSourcePersistenceType persistenceType);

        protected abstract void CreateRepositories(Dictionary<string, object> repositories);

        protected abstract void CreateConstraints(Dictionary<string, IConstraint> constraints);

        private void PerformSetup()
        {
            foreach (IConstraint constraint in Constraints)
            {
                if (constraint.RequiresSetup)
                {
                    constraint.Setup();
                }
            }
        }

        #region IDomainModel Implementation

        public DataSourcePersistenceType PersistenceType
        {
            get { return m_DataProvider.PersistenceType; }
        }

        public IDataProvider DataProvider
        {
            get { return m_DataProvider; }
        }

        public IDictionary<DataSourcePersistenceType, Type> DataSourceTypesForPersistenceTypes
        {
            get { return new Dictionary<DataSourcePersistenceType, Type>(m_DataSourceTypesForPersistenceTypes); }
        }

        public IList<IConstraint> Constraints
        {
            get { return new List<IConstraint>(m_Constraints.Values); }
        }

        public DATA_PROVIDER GetDataProvider<DATA_PROVIDER>()
            where DATA_PROVIDER : IDataProvider
        {
            if (m_DataProvider == null)
            { return default(DATA_PROVIDER); }

            if (!m_DataProvider.GetType().Equals(typeof(DATA_PROVIDER)))
            { throw new ApplicationException(); }

            return (DATA_PROVIDER)m_DataProvider;
        }

        public IDataProvider GetDataProvider()
        {
            return m_DataProvider;
        }

        public bool HasConstraint<CONSTRAINT>()
           where CONSTRAINT : IConstraint
        {
            string typeKey = typeof(CONSTRAINT).FullName;
            bool containsKey = m_Constraints.ContainsKey(typeKey);
            return containsKey;
        }

        public CONSTRAINT GetConstraint<CONSTRAINT>()
            where CONSTRAINT : IConstraint
        {
            string typeKey = typeof(CONSTRAINT).FullName;
            if (!m_Constraints.ContainsKey(typeKey))
            {
                StackTrace stackTrace = new StackTrace();
                string className = GetType().Name;
                string methodName = stackTrace.GetFrame(0).GetMethod().Name;
                string message = string.Format("Call to method \"{0}.{1}\" failed because requested IConstraint Type \"{2}\" does not exist in domain model.", className, methodName, typeKey.ToString());
                throw new DomainModelUsageException(message);
            }

            IConstraint untypedConstraint = m_Constraints[typeKey];
            if (!untypedConstraint.GetType().Equals(typeof(CONSTRAINT)))
            {
                StackTrace stackTrace = new StackTrace();
                string className = GetType().Name;
                string methodName = stackTrace.GetFrame(0).GetMethod().Name;
                string message = string.Format("Call to method \"{0}.{1}\" failed because requested IConstraint Type \"{2}\" does not exist in domain model.", className, methodName, typeKey.ToString());
                throw new DomainModelUsageException(message);
            }

            return (CONSTRAINT)untypedConstraint;
        }

        public bool HasRepository<KEY, KEYED_DOMAIN_OBJECT>()
            where KEYED_DOMAIN_OBJECT : IKeyedDomainObject<KEY, KEYED_DOMAIN_OBJECT>
        {
            string typeKey = typeof(KEYED_DOMAIN_OBJECT).FullName;
            bool containsKey = m_Repositories.ContainsKey(typeKey);
            return containsKey;
        }

        public IRepository<KEY, KEYED_DOMAIN_OBJECT> GetRepository<KEY, KEYED_DOMAIN_OBJECT>()
            where KEYED_DOMAIN_OBJECT : IKeyedDomainObject<KEY, KEYED_DOMAIN_OBJECT>
        {
            string typeKey = typeof(KEYED_DOMAIN_OBJECT).FullName;

            if (!m_Repositories.ContainsKey(typeKey))
            {
                StackTrace stackTrace = new StackTrace();
                string className = GetType().Name;
                string methodName = stackTrace.GetFrame(0).GetMethod().Name;
                string message = string.Format("Call to method \"{0}.{1}\" failed because no repository exists in the domain model for the requested IDomainObject Type \"{2}\".", className, methodName, typeKey.ToString());
                throw new DomainModelUsageException(message);
            }

            object untypedRepository = m_Repositories[typeKey];
            IRepository<KEY, KEYED_DOMAIN_OBJECT> typedRepository = untypedRepository as IRepository<KEY, KEYED_DOMAIN_OBJECT>;

            if (typedRepository == null)
            {
                StackTrace stackTrace = new StackTrace();
                string className = GetType().Name;
                string methodName = stackTrace.GetFrame(0).GetMethod().Name;
                string message = string.Format("Call to method \"{0}.{1}\" failed because no repository exists in the domain model for the requested IDomainObject Type \"{2}\".", className, methodName, typeKey.ToString());
                throw new DomainModelUsageException(message);
            }

            return typedRepository;
        }

        public TRANSACTION_SCRIPT CreateTransactionScript<TRANSACTION_SCRIPT>()
            where TRANSACTION_SCRIPT : ITransactionScript, new()
        {
            TRANSACTION_SCRIPT transactionScript = new TRANSACTION_SCRIPT();
            transactionScript.DomainModel = this;
            return transactionScript;
        }

        #endregion

        #region IBatchPersistableDomainModel Implementation

        public event ChangingDataSourceDelegate LoadingFromDataSource;
        public event ChangedDataSourceDelegate LoadedFromDataSource;
        public event ChangingDataSourceDelegate PersistingToDataSource;
        public event ChangedDataSourceDelegate PersistedToDataSource;

        public InvalidCallAction InvalidCallAction
        {
            get { return m_InvalidCallAction; }
        }

        public virtual void LoadFromDataSource<DATASOURCE>(DATASOURCE dataSource)
        {
            if (PersistenceType != DataSourcePersistenceType.File)
            {
                if (InvalidCallAction == InvalidCallAction.ThrowException)
                { throw new ApplicationException("The domain model does not support batch persistence."); }
                else
                { return; }
            }
            if (!dataSource.GetType().Equals(DataProvider.DataSourceType))
            {
                throw new ApplicationException("The supplied datasource type does not match the type currently in use.");
            }

            DataProvider.DataSource = dataSource;
            Dictionary<IConstraint, bool> constraintsEnabled = new Dictionary<IConstraint, bool>();
            foreach (IConstraint constraint in Constraints)
            {
                constraintsEnabled[constraint] = constraint.Enabled;
                constraint.Enabled = false;
            }

            if (LoadingFromDataSource != null)
            {
                LoadingFromDataSource(this, new EventArgs());
            }
            if (LoadedFromDataSource != null)
            {
                LoadedFromDataSource(this, new EventArgs());
            }

            foreach (IConstraint constraint in Constraints)
            {
                constraint.Enabled = constraintsEnabled[constraint];
            }
            DataProvider.DataSource = null;
        }

        public virtual void PersistToDataSource<DATASOURCE>(DATASOURCE dataSource)
        {
            if (PersistenceType != DataSourcePersistenceType.File)
            {
                if (InvalidCallAction == InvalidCallAction.ThrowException)
                { throw new ApplicationException("The domain model does not support batch persistence."); }
                else
                { return; }
            }
            if (!dataSource.GetType().Equals(DataProvider.DataSourceType))
            {
                throw new ApplicationException("The supplied datasource type does not match the type currently in use.");
            }

            DataProvider.DataSource = dataSource;
            DataSet dataSourceAsDataSet = (dataSource as DataSet);
            if (dataSourceAsDataSet != null)
            { dataSourceAsDataSet.EnforceConstraints = false; }

            if (PersistingToDataSource != null)
            {
                PersistingToDataSource(this, new EventArgs());
            }
            if (PersistedToDataSource != null)
            {
                PersistedToDataSource(this, new EventArgs());
            }

            if (dataSourceAsDataSet != null)
            {
                dataSourceAsDataSet.EnforceConstraints = true;
                dataSourceAsDataSet.AcceptChanges();
            }
            DataProvider.DataSource = null;
        }

        #endregion
    }
}